/* 
 * File:   Filesystem.cpp
 * Author: pkv
 * 
 * Created on August 4, 2011, 3:37 PM
 */

#include "Filesystem.h"

#include <physfs.h>
#include <streambuf>
#include <cstring>
#include <iostream>

Mutex Filesystem::_mutex;
int Filesystem::_numInstances = 0;

SLB3_IMPLEMENTATION(Filesystem, C) {
  C.constructor()
      /*      .set("getPathSeparator", &Filesystem::getPathSeparator)
      .set("useSymbolicLinks", &Filesystem::useSymbolicLinks)
      .set("getBaseDir", &Filesystem::getBaseDir)
      .set("getUserDir", &Filesystem::getUserDir)
      .set("getWriteDir", &Filesystem::getWriteDir)
      .set("setWriteDir", &Filesystem::setWriteDir)
      .set("mount", &Filesystem::mount)
      .set("unmount", &Filesystem::unmount)
      .set("getSearchPath", &Filesystem::getSearchPath)
      .set("setSaneConfig", &Filesystem::setSaneConfig)
      .set("mkdir", &Filesystem::mkdir)
      .set("del", &Filesystem::del)
      .set("getPath", &Filesystem::getPath)
      .set("enumerateFiles", &Filesystem::enumerateFiles)
      .set("exists", &Filesystem::exists)
      .set("isDirectory", &Filesystem::isDirectory)
      .set("isSymbolicLink", &Filesystem::isSymbolicLink)
      .set("openWrite", &Filesystem::openWrite)
      .set("openRead", &Filesystem::openRead)
      .set("openTemp", &Filesystem::openTemp)*/
      ;
}

Filesystem::Filesystem()
{
  Lock lock(_mutex);
  
  if (_numInstances == 0) {
    PHYSFS_init(NULL);    
  }
  _numInstances++;
}

Filesystem::~Filesystem()
{
  Lock lock(_mutex);

  _numInstances--;
  if (_numInstances == 0) {
    PHYSFS_deinit();
  }
}

std::string Filesystem::getPathSeparator()
{
  return std::string(PHYSFS_getDirSeparator());
}

void Filesystem::useSymbolicLinks(bool allow)
{
  PHYSFS_permitSymbolicLinks(allow);
}

std::string Filesystem::getBaseDir()
{
  return std::string(PHYSFS_getBaseDir());
}

std::string Filesystem::getUserDir()
{
  return std::string(PHYSFS_getUserDir());
}

std::string Filesystem::getWriteDir()
{
  return std::string(PHYSFS_getWriteDir());
}

int Filesystem::setWriteDir(std::string newDir)
{
  return PHYSFS_setWriteDir(newDir.c_str());
}

int Filesystem::mount(std::string newDir, std::string mountPoint, bool appendToPath)
{
  return PHYSFS_mount(newDir.c_str(), mountPoint.c_str(), appendToPath);
}

int Filesystem::unmount(std::string oldDir)
{
  return PHYSFS_removeFromSearchPath(oldDir.c_str());
}

std::vector<std::string> Filesystem::getSearchPath()
{
  std::vector<std::string> paths;

  char** searchPath = PHYSFS_getSearchPath();
  for (char** i = searchPath; *i != 0; ++i) {
    paths.push_back(*i);
  }
  PHYSFS_freeList(searchPath);
  
  return paths;
}

int Filesystem::setSaneConfig(std::string organization, std::string appName, std::string archiveExt, int includeCdRoms, int archivesFirst)
{
  return PHYSFS_setSaneConfig(organization.c_str(), appName.c_str(), archiveExt.c_str(), includeCdRoms, archivesFirst);
}

int Filesystem::mkdir(std::string dirName)
{
  return PHYSFS_mkdir(dirName.c_str());
}

int Filesystem::del(std::string filename)
{
  return PHYSFS_delete(filename.c_str());
}

std::string Filesystem::getPath(std::string filename)
{
  return std::string(PHYSFS_getRealDir(filename.c_str()));
}

std::vector<std::string> Filesystem::enumerateFiles(std::string dir)
{
  std::vector<std::string> files;

  char** directory = PHYSFS_enumerateFiles(dir.c_str());
  for (char** i = directory; *i != 0; ++i) {
    files.push_back(*i);
  }
  PHYSFS_freeList(directory);
  
  return files;
}

int Filesystem::exists(std::string filename)
{
  return PHYSFS_exists(filename.c_str());
}

bool Filesystem::isDirectory(std::string filename)
{
  return PHYSFS_isDirectory(filename.c_str());
}

bool Filesystem::isSymbolicLink(std::string filename)
{
  return PHYSFS_isSymbolicLink(filename.c_str());
}

////// FILE WRITING //////

/**
 * Wrapper that converts from PHYSFS_File to a normal std::streambuf, to allow
 * more compatible IO operations.
 */
class FilesystemOutputStreamBuf : public std::streambuf
{
public:

    FilesystemOutputStreamBuf(std::string filename, bool append, size_t bufferSize = size_t(1000))
            : _buffer(bufferSize + 1)
    {
        // Open the file for reading or appending
        // TODO: add error handling here!
        _file = append ? PHYSFS_openAppend(filename.c_str()) : PHYSFS_openWrite(filename.c_str());
        
        // Set up the correct pointer index for the stream
        char *base = &_buffer.front();
        setp(base, base + _buffer.size() - 1); // -1 to make overflow() easier
    }

    ~FilesystemOutputStreamBuf()
    {
        sync();
        PHYSFS_close(_file);
    }

private:
    PHYSFS_File *_file;
    std::vector<char> _buffer;

    // When the buffer fills, empty it to the file
    int_type overflow(int_type ch)
    {
        // If the file is valid and we are not at the end of it
        if (_file && ch != traits_type::eof())
        {
            assert(std::less_equal<char *>()(pptr(), epptr()));
            *pptr() = ch;
            pbump(1);
            if (writeAndFlush())
                return ch;
        }

        // Otherwise, say that we hit the end of the file
        return traits_type::eof();
    }

    // Force the buffer to be pushed to file
    int sync()
    {
        // Should return a -1 on failure
        return (_file && writeAndFlush()) ? 0 : -1;
    }

    // Do the actual processing
    bool writeAndFlush()
    {
        std::ptrdiff_t n = pptr() - pbase();
        pbump(-n);

        PHYSFS_sint64 result = PHYSFS_write( _file, pbase(), 1, n);
        PHYSFS_flush(_file);
        
        return (result == n);
    }
    
    // Copying is not allowed
    FilesystemOutputStreamBuf(const FilesystemOutputStreamBuf &);
    FilesystemOutputStreamBuf &operator=(const FilesystemOutputStreamBuf &);
};

////// FILE READING //////

/**
 * Wrapper that converts from PHYSFS_File to a normal std::streambuf, to allow
 * more compatible IO operations.
 * 
 * Adapted from: http://www.mr-edd.co.uk/blog/beginners_guide_streambuf
 */
class FilesystemInputStreamBuf : public std::streambuf
{
public:

    FilesystemInputStreamBuf(std::string filename, size_t bufferSize = size_t(1000), size_t putBackSize = size_t(1))
    : _putBackSize(std::max(putBackSize, size_t(1))), _buffer(std::max(bufferSize, putBackSize) + putBackSize)
    {
        // Open the file for reading
        // TODO: add error handling here!
        _file = PHYSFS_openRead(filename.c_str());

        // Set up the correct pointer index for the stream
        char *end = &_buffer.front() + _buffer.size();
        setg(end, end, end);
    }

    ~FilesystemInputStreamBuf()
    {
        PHYSFS_close(_file);
    }

private:
    PHYSFS_File *_file;
    const std::size_t _putBackSize;
    std::vector<char> _buffer;

    // Copying is not allowed
    FilesystemInputStreamBuf(const FilesystemInputStreamBuf &);
    FilesystemInputStreamBuf &operator=(const FilesystemInputStreamBuf &);

    // When the buffer empties, refill it from the file
    int_type underflow()
    {
         // If the buffer is not exhausted, just return the pointer to it
        if (gptr() < egptr())
            return traits_type::to_int_type(*gptr());

        // If file is invalid, just return EOF
        if (!_file)
            return traits_type::eof();
        
        // Get the start of the buffer
        char *base = &_buffer.front();
        char *start = base;

         // If this isn't the first fill
        if (eback() == base)
        {
            // Shift over the last buffer worth of putback characters
            std::memmove(base, egptr() - _putBackSize, _putBackSize);
            start += _putBackSize;
        }

        // Start is now set to the start of the normal buffer
        // Read from file into the buffer
        PHYSFS_sint64 n = PHYSFS_read(_file, start, 1, _buffer.size() - (start - base));
        if (n == 0 && PHYSFS_eof(_file))
            return traits_type::eof();

        // Set buffer pointers
        setg(base, start, start + n);

        return traits_type::to_int_type(*gptr());
    }
};

std::auto_ptr<std::ostream> Filesystem::openWrite(std::string filename, bool append)
{
  return std::auto_ptr<std::ostream>(new std::ostream(new FilesystemOutputStreamBuf(filename, append)));
}

std::auto_ptr<std::istream> Filesystem::openRead(std::string filename)
{
  return std::auto_ptr<std::istream>(new std::istream(new FilesystemInputStreamBuf(filename)));
}

std::string Filesystem::openTemp(std::string filename)
{
  std::string tempFile;
    
  // Open the file for reading
  PHYSFS_File *_file = PHYSFS_openRead(filename.c_str());
  if (!_file)
    return tempFile;
  
  // Open a temp file for writing
  tempFile = std::string(tmpnam(NULL));
  FILE *fd = fopen(tempFile.c_str(), "w");
  if (!fd)
    return tempFile;
  
  // Copy the contents of the PHYSFS file to the temporary file
  char buffer[1024];
  while (!PHYSFS_eof(_file)) {
    PHYSFS_sint64 bytesRead = PHYSFS_read(_file, buffer, 1, 1024);
    fwrite(buffer, 1, bytesRead, fd);
  }
  fclose(fd);
    
  // Return the filename where a copy of the file resides
  return tempFile;
}
